package cn.sciento.boot.api.customize.generator;

import com.google.common.base.Charsets;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.NotFoundException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.groovy.util.CharSequenceReader;
import cn.sciento.boot.api.customize.agent.HotSwapClient;
import cn.sciento.boot.api.customize.commons.handler.ICustomizeHandler;
import cn.sciento.boot.api.customize.commons.vo.ApiCustomizeRule;
import cn.sciento.boot.api.customize.compile.DynamicCompiler;
import cn.sciento.boot.api.customize.config.CustomizeProperties;
import cn.sciento.boot.api.customize.exception.ClassDefinedException;
import cn.sciento.boot.api.customize.exception.InterfaceWrongException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;

public class JavaCodeGenerator extends AbstractCodeGenerator {
  private static final Logger LOGGER = LoggerFactory.getLogger(JavaCodeGenerator.class);
  
  private static final String LAUNCHED_CLASS_LOADER = "org.springframework.boot.loader.LaunchedURLClassLoader";
  
  private static final String TYPE = "JAVA";
  
  private static final String CUSTOMIZE_SRC_DIR = StringUtils.join((Object[])new String[] { "customize", File.separator, "src", File.separator });
  
  private static final String CUSTOMIZE_CLZ_DIR = StringUtils.join((Object[])new String[] { "customize", File.separator, "classes", File.separator });
  
  @Value("${spring.application.name}")
  private String appName;
  
  public JavaCodeGenerator(CustomizeProperties properties) {
    if (properties.isEnableAgent())
      try {
        HotSwapClient.startAgent();
      } catch (Exception e) {
        LOGGER.error("start hot swap agent failure, please check if include tools.jar. ex = {}", e.getMessage());
      }  
  }
  
  public boolean support(String type) {
    return "JAVA".equalsIgnoreCase(type);
  }
  
  public String generate(ClassPool classPool, CtClass targetClass, CtMethod targetMethod, ApiCustomizeRule rule, boolean returnValue) throws Exception {
    String sourceCode = rule.getTypeValue();
    if (StringUtils.isBlank(sourceCode)) {
      LOGGER.error("customize rule source code is blank, ruleCode is {}", rule.getRuleCode());
      throw new IllegalArgumentException("customize.compile.ruleSourceCodeIsBlank");
    } 
    ClassInfo classInfo = createJavaFile(sourceCode);
    File classFile = compileJavaFile(classInfo);
    CtClass executeClass = classPool.makeClass(new FileInputStream(classFile));
    try {
      ClassLoader loader = getClassLoader();
      executeClass.toClass(loader, null);
    } catch (Exception e) {
      if (e.getMessage().contains("duplicate class definition")) {
        LOGGER.warn("customize rule code compile failed, duplicate class definition. ruleCode is {}, ex = {}", rule.getRuleCode(), e.getMessage());
      } else {
        LOGGER.error("customize rule code compile failed, ruleCode is {}", rule.getRuleCode(), e);
        throw new IllegalArgumentException("customize.ruleCode.compileFailed");
      } 
    } 
    if (executeClass.getInterfaces() == null || 
      Arrays.<CtClass>stream(executeClass.getInterfaces()).noneMatch(in -> in.getName().equals(ICustomizeHandler.class.getName()))) {
      LOGGER.error("customize rule code should implements ICustomizeHandler, ruleCode is {}", rule.getRuleCode());
      throw new InterfaceWrongException("customize.ruleCode.implICustomizeHandler");
    } 
    executeClass.getDeclaredMethod("execute");
    StringBuilder executeBody = new StringBuilder();
    builderExecuteBody(executeBody, executeClass, returnValue);
    if (rule.getTenantId() != null && rule.getTenantId().longValue() != 0L)
      buildTenantBody(executeBody, rule.getTenantId()); 
    if (!returnValue && (rule.getSyncFlag() == null || rule.getSyncFlag().equals(Integer.valueOf(0))))
      buildAsyncBody(executeBody, classPool, executeClass); 
    return executeBody.toString();
  }
  
  private ClassLoader getClassLoader() {
    ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
    if (!"org.springframework.boot.loader.LaunchedURLClassLoader".equals(contextClassLoader.getClass().getName()))
      if ("org.springframework.boot.loader.LaunchedURLClassLoader".equals(contextClassLoader.getParent().getClass().getName())) {
        contextClassLoader = contextClassLoader.getParent();
      } else {
        contextClassLoader = ClassLoader.getSystemClassLoader();
      }  
    return contextClassLoader;
  }
  
  private ClassInfo createJavaFile(String sourceCode) throws ClassDefinedException, IOException {
    ClassInfo classInfo;
    String packageName = null;
    String className = null;
    String originalClassName = null;
    try(StringWriter writer = new StringWriter(); 
        BufferedReader reader = new BufferedReader((Reader)new CharSequenceReader(sourceCode))) {
      String line = null;
      while ((line = reader.readLine()) != null) {
        if (packageName == null && line.contains("package"))
          packageName = StringUtils.trim(StringUtils.substringBetween(sourceCode, "package", ";")); 
        if (originalClassName == null && line.contains("class") && (line.contains("extend") || line.contains("implements"))) {
          if (line.contains("extend")) {
            originalClassName = StringUtils.trim(StringUtils.substringBetween(sourceCode, "class", "extend"));
          } else if (line.contains("implements")) {
            originalClassName = StringUtils.trim(StringUtils.substringBetween(sourceCode, "class", "implements"));
          } 
          className = originalClassName + RandomUtils.nextInt();
          line = StringUtils.replace(line, originalClassName, className);
        } 
        writer.write(line);
        writer.write("\r\n");
      } 
      if (className == null)
        throw new ClassDefinedException("class name not found."); 
      if (packageName == null)
        throw new ClassDefinedException("package name name not found."); 
      LOGGER.info("parse package name : {}, original class name : {}, random class name : {}", new Object[] { packageName, originalClassName, className });
      classInfo = new ClassInfo(packageName, className, originalClassName, writer.getBuffer().toString());
    } 
    File javaFile = new File(CUSTOMIZE_SRC_DIR + StringUtils.replace(classInfo.getClassFullName(), ".", File.separator) + ".java");
    FileUtils.writeByteArrayToFile(javaFile, classInfo.getSourceCode().getBytes(Charsets.UTF_8));
    classInfo.setClasspath(javaFile.getPath());
    return classInfo;
  }
  
  private File compileJavaFile(ClassInfo classInfo) throws Exception {
    DynamicCompiler dynamicCompiler = new DynamicCompiler(Thread.currentThread().getContextClassLoader());
    dynamicCompiler.addSource(classInfo.getClassname(), classInfo.getSourceCode());
    Map<String, byte[]> byteCodes = dynamicCompiler.buildByteCodes();
    File outputDir = new File(CUSTOMIZE_CLZ_DIR);
    File classFile = null;
    Iterator<Map.Entry<String, byte[]>> iterator = byteCodes.entrySet().iterator();
    if (iterator.hasNext()) {
      Map.Entry<String, byte[]> entry = iterator.next();
      classFile = new File(outputDir, StringUtils.replace(entry.getKey(), ".", File.separator) + ".class");
      FileUtils.writeByteArrayToFile(classFile, entry.getValue());
    } 
    LOGGER.info("customize rule code compile [{}] success.", classInfo.getClasspath());
    return classFile;
  }
  
  private void builderExecuteBody(StringBuilder executeBody, CtClass executeClass, boolean returnValue) {
    executeBody
      .append(executeClass.getName())
      .append(" ")
      .append(executeClass.getSimpleName())
      .append(" = ")
      .append("new ").append(executeClass.getName()).append("();")
      .append("\r\n");
    if (returnValue)
      executeBody.append("Object result = "); 
    executeBody
      .append(executeClass.getSimpleName()).append(".execute($args);")
      .append("\r\n");
    if (returnValue)
      executeBody.append("return ($r) result;").append("\r\n"); 
  }
  
  private void buildTenantBody(StringBuilder executeBody, Long tenantId) {
    StringBuilder builder = new StringBuilder();
    builder
      .append("if (")
      .append("new Long(\"").append(tenantId).append("\")")
      .append(".equals(")
      .append("cn.sciento.core.oauth.DetailsHelper.getUserDetails().getTenantId()))")
      .append(" {")
      .append("\r\n");
    executeBody.insert(0, builder);
    executeBody.append("}");
  }
  
  private void buildAsyncBody(StringBuilder executeBody, ClassPool classPool, CtClass executeClass) throws NotFoundException, CannotCompileException {
    String runnableName = "cn.sciento.customize.runnable." + executeClass.getSimpleName() + "Runnable";
    CtClass runnableClass = classPool.makeClass(runnableName);
    runnableClass.addInterface(classPool.get(Runnable.class.getName()));
    StringBuilder runBody = new StringBuilder();
    runBody
      .append("public void run() {").append("\r\n")
      .append(executeBody.toString())
      .append("}")
      .append("\r\n");
    CtMethod runMethod = CtNewMethod.make(runBody.toString(), runnableClass);
    runnableClass.addMethod(runMethod);
    try {
      runnableClass.toClass();
    } catch (CannotCompileException e) {
      if (e.getMessage().contains("duplicate class definition")) {
        LOGGER.warn("customize runnable class duplicate. ex = {}", e.getMessage());
      } else {
        LOGGER.error("customize runnable class compile failed.", (Throwable)e);
        throw new IllegalArgumentException("customize.ruleCode.customRunnableClassCompileFailed");
      } 
    } 
    executeBody.delete(0, executeBody.length());
    String varKey = executeClass.getSimpleName() + "Thread";
    executeBody
      .append("java.lang.Thread").append(" ").append(varKey)
      .append(" = ")
      .append("new java.lang.Thread(new ").append(runnableName).append("(), \"Thread-").append(RandomUtils.nextInt()).append("-async-customize\");")
      .append("\r\n")
      .append(varKey).append(".start();")
      .append("\r\n");
  }
  
  static class ClassInfo {
    private String packageName;
    
    private String classname;
    
    private String originalClassname;
    
    private String sourceCode;
    
    private String classFullName;
    
    private String classpath;
    
    ClassInfo(String packageName, String classname, String originalClassname, String sourceCode) {
      this.packageName = packageName;
      this.classname = classname;
      this.originalClassname = originalClassname;
      this.sourceCode = sourceCode;
      this.classFullName = packageName + "." + classname;
    }
    
    public String getPackageName() {
      return this.packageName;
    }
    
    public String getClassname() {
      return this.classname;
    }
    
    public String getOriginalClassname() {
      return this.originalClassname;
    }
    
    public String getSourceCode() {
      return this.sourceCode;
    }
    
    public String getClassFullName() {
      return this.classFullName;
    }
    
    public String getClasspath() {
      return this.classpath;
    }
    
    public void setClasspath(String classpath) {
      this.classpath = classpath;
    }
  }
}
